package de.ghost23.common.process {
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	import flash.utils.getDefinitionByName;
	import flash.utils.getQualifiedClassName;

	/**
	 * The BranchProcess is an extension of the simpler Process class (although it does not extend Process
	 * in code, because some of the methods differ too much). As well as
	 * the simpler Process class, it can be either be used by extending it, or used
	 * at runtime by using the methods 'pushCommandStep' and 'pushGeneralFollower'. In comparison to
	 * the Process class, a BranchProcess can have different branches in its queue. So,
	 * a BranchProcess is not linear, but goes in different directions, depending on how
	 * an individual Command has finished. Therefor, the BranchProcess does not use the
	 * normal Command classes, but instead the ACommand.
	 *
	 * <p>The difference is, that
	 * the ACommand returns a 'resultCode' after having finished. This is basically
	 * a String. The BranchProcess has a mapping queue, that maps the resultCode to a
	 * specific Follower, which is the ACommand, that should follow after the previous
	 * one, but only, if the previous one had finished with this specific code.</p>
	 * @example
	 * The following simplified code example should make the concept clear:
	<listing version="3.0">tree = new Dictionary();
tree[SampleCommandOne] = new Dictionary();
tree[SampleCommandOne]["200"] = SampleCommandTwo;
tree[SampleCommandOne]["500"] = SampleGeneralError;

tree[SampleCommandTwo] = new Dictionary();
tree[SampleCommandTwo]["200"] = SampleCommandThree;
tree[SampleCommandTwo]["210"] = SampleCommandOne;
tree[SampleCommandTwo]["500"] = SampleGeneralError;

tree[SampleCommandThree] = new Dictionary();
tree[SampleGeneralError] = new Dictionary();</listing>
	 * <p>Note, that a Step defines all the ACommands, that are allowed to occur in this
	 * BranchProcess. You might wonder, why the last two Steps have to be defined. It is,
	 * because the BranchProcess will only execute ACommands, that have been defined
	 * officially as Steps within this BranchProcess. Thus, by looking only at the steps,
	 * you see all ACommands, that are involved in this BranchProcess.</p>
	 *
	 * <p>Also note, that Steps and a Followers are made up of references to the command classes directly, not class instances.
	 * BranchProcess uses lazy instantiation, so an instance of the respective command is created directly
	 * before executing the command.</p>
	 *
	 * <p>If a Step has no Followers defined, then this BranchProcess will end, after the
	 * DialogCommand has ended.</p>
	 *
	 * <p>Since it is very likely, that there might be ACommands, that CAN be Followers
	 * of every previous ACommand (such as GeneralError for example), the BranchProcess
	 * class introduces the generalFollowers queue. Let's take a look, how the example above
	 * can look like, when using the generalFollowers queue:</p>
	<listing version="3.0">generalFollowers = new Dictionary();
generalFollowers["500"] = SampleGeneralError;

tree = new Dictionary();
tree[SampleCommandOne] = new Dictionary();
tree[SampleCommandOne]["200"] = SampleCommandTwo;

tree[SampleCommandTwo] = new Dictionary();
tree[SampleCommandTwo]["200"] = SampleCommandThree;
tree[SampleCommandTwo]["210"] = SampleCommandOne;

tree[SampleCommandThree] = new Dictionary();
tree[SampleGeneralError] = new Dictionary();</listing>
	 * <p>You can see, that the SampleCommandOne and SampleCommandTwo no longer define
	 * SampleGeneralError as a Follower for the resultCode '500', as opposed to the first
	 * example. Instead now, each one of the four defined Steps (yes, even the SampleGeneralError
	 * itself) implicitly has the SampleGeneralError defined as a Follower for the
	 * resultCode '500'. This was introduced as an alternative for having to type less and
	 * also for making clear, that some ACommands are so generic, they will always
	 * be defined as possible Followers.</p>
	 *
	 * <p>When using this BranchProcess by extending it, a concrete BranchProcess class
	 * could look like this. Basically, only the constructor has to be overridden (import
	 * statements have been left out):</p>
	 * @example
	<listing version="3.0">class de.ghost23.processtest.SampleProcess extends BranchProcess {

	public function SampleProcess() {

		super();

		generalFollowers = new Dictionary();
		generalFollowers["500"] = SampleGeneralError;

		tree = new Dictionary();
		tree[SampleCommandOne] = new Dictionary();
		tree[SampleCommandOne]["200"] = SampleCommandTwo;

		tree[SampleCommandTwo] = new Dictionary();
		tree[SampleCommandTwo]["200"] = SampleCommandThree;
		tree[SampleCommandTwo]["210"] = SampleCommandOne;

		tree[SampleCommandThree] = null;
		tree[SampleGeneralError] = null;
	}
}</listing>
	 * <p>When using the BranchProcess at runtime, it could look like this (import
	 * statements and variable declarations have been left out):</p>
	 * @example
	<listing version="3.0">var sampleProcess:BranchProcess = new BranchProcess();
sampleProcess.pushGeneralFollower(new Follower("500", SampleGeneralError));

sampleProcess.pushStep(
	new Step(
		SampleCommandOne,
		[
			new Follower("200", SampleCommandTwo)
		]
	)
);

var tmpStep:Step = new Step(SampleCommandTwo);
tmpStep.pushFollower(new Follower("200", SampleCommandThree));
tmpStep.pushFollower(new Follower("210", SampleCommandOne));
sampleProcess.pushStep(tmpStep);

sampleProcess.pushStep(new Step(SampleCommandThree));
sampleProcess.pushStep(new Step(SampleGeneralError));

sampleProcess.addEventListener(ProcessEvent.COMPLETED, this);
sampleProcess.addEventListener(ProcessEvent.FAILED, this);
sampleProcess.start();</listing>
	 * <p>Since BranchProcess is quite similar to Process, please also look at the Process class for methods like
	 * start, stop and clear.</p>
	 * @author Sven Busse
	 * @version 1.1
	 * @see de.ghost23.common.process.Process
	 */
	public class BranchProcess extends EventDispatcher {
		
		/**
		 * General Followers with DialogCommands, which can be triggered always.
		 */
		protected var generalFollowers:Dictionary;

		/**
		 * BranchProcess uses a tree instead of queue.
		 */
		protected var tree:Dictionary;

		/**
		* The currently pending ICommand
		*/
		private var currentCmd:ICommand;

		/**
		 * Holder for various param objects or primitives, that are needed by
		 * either the process or the commands. This Dictionary is passed to every
		 * command, once it is executed. So, this way, command can exchange data,
		 * or data can be passed to and from them from outside of the process.
		 */
		private var _data:Dictionary;

		/**
		* Starts the process. Do not forget to add Listeners to the ProcessEvent.COMPLETED and
		* ProcessEvent.FAILED events before calling start, in case your Process is synchronized.
		*/
		public function start(commandToStartWith:ICommand):void {
			reset();
			currentCmd = commandToStartWith;
			doNext(null);
		}

		/**
		 * Stops this process, calls cancel at the currently running Command, BUT does not
		 * listen to the FAILED event of that command anymore. Thus, this process
		 * will not dispatch a ProcessEvent.FAILED.
		 */
		public function stop():void {
			reset();
		}

		/**
		 * Clear does also stop the Process in case it was running (see stop for details). In addition,
		 * the qeue and the ParamContainer is set to null. This means, you cannot start this Process
		 * again, unless you fill the queue again by using pushCommandStep.
		 */
		public function clear():void {
			reset();
			tree = null;
			_data = null;
		}
		
		/**
		 * Returns the currently active DialogCommand
		 */
		public function get currentDialogCommand():ICommand {
			return currentCmd;
		}
		
		/**
		 * For runtime use. Add new general Followers here. These will be
		 * automatically defined as possible Followers for every DialogCommand in the
		 * queue.
		 * @param	newFollower The new Follower
		 */
		public function pushGeneralFollower(newFollower:Follower):void {
			
			if (generalFollowers == null) generalFollowers = new Dictionary();
			generalFollowers[newFollower.code] = newFollower.command;
		}
		
		/**
		 * For runtime use. Add the steps here, that should make up your
		 * BranchProcess.
		 * @param	newStep A new step in the BranchProcess
		 */
		public function pushStep(newStep:Step):void {
			
			if (tree == null) tree = new Dictionary();
			tree[newStep.command] = new Dictionary();
			for(var i:int = 0; i < newStep.followers.length; i++) {
				tree[newStep.command][newStep.followers[i].code] = newStep.followers[i].command;
			}
		}

		/**
		 * Holder for various param objects or primitives, that are needed by
		 * either the process or the commands. This Dictionary is passed to every
		 * command, once it is executed. So, this way, command can exchange data,
		 * or data can be passed to and from them from outside of the process.
		 */
		public function get data():Dictionary {
			return _data;
		}
		
		/**
		 * Processes the next DialogCommand.
		 */
		private function doNext(evt:ProcessEvent):void {
			
			//trace("BranchProcess.doNext");

			currentDialogCommand.removeEventListener(ProcessEvent.COMPLETED, doNext);
			currentDialogCommand.removeEventListener(ProcessEvent.FAILED, onCommandFailed);
			try {
				currentCmd = searchNextCommand(currentDialogCommand, evt.resultCode);
			}catch(e:Error) {
				dispatchEvent(new ProcessEvent(ProcessEvent.FAILED, null, getClass(currentDialogCommand)));
				return;
			}
			
			if(currentDialogCommand == null) {
				// There is no command left, so we're finished :o)
				end(evt.resultCode);
				
			}else {
				currentDialogCommand.addEventListener(ProcessEvent.COMPLETED, doNext, false, 0, true);
				currentDialogCommand.addEventListener(ProcessEvent.FAILED, onCommandFailed, false, 0, true);
				currentDialogCommand.execute(_data);
			}
		}
		
		/**
		 * Searches for a command in the queue by processing the given command and resultcode.
		 * @param	finishedCommand The DialogCommand, that has just yet finished
		 * @param	resultCode The result code, with which the command has finished.
		 * @return The command, that has been found or null, if none has been found.
		 */
		private function searchNextCommand(finishedCommand:ICommand, resultCode:String):ICommand {

			if(tree[finishedCommand] == null && generalFollowers[resultCode] == undefined) {
				return null;

			}else if(tree[finishedCommand][resultCode] != undefined) {
				return new tree[finishedCommand][resultCode]();

			}else if(generalFollowers[resultCode] != undefined) {
				return new generalFollowers[resultCode]();
				
			}else {
				throw new Error("BranchProcess.searchNextCommand. code mismatch: " + resultCode);
			}
		}
		
		/**
		 * When the BranchProcess has finished, this method is called. Dispatches the
		 * ProcessEvent.COMPLETED event with the resultCode, that the last DialogCommand
		 * had given.
		 * @param	endResultCode The resultCode of the last DialogCommand.
		 */
		private function end(endResultCode:String):void {
			reset();
			dispatchEvent(new ProcessEvent(ProcessEvent.COMPLETED, endResultCode, null));
		}

		/**
		 * If an ICommand dispatches ProcessEvent.FAILED, then reset is called and this Process
		 * dispatches ProcessEvent.FAILED, passing through a reference to the ICommand, that
		 * failed.
		 * @param	evt EventHandler argument, the event object
		 */
		private function onCommandFailed(evt:ProcessEvent):void {
			reset();
			dispatchEvent(new ProcessEvent(ProcessEvent.FAILED, null, getClass(evt.target)));
		}

		/**
		 * Removes the listener of the currently running ICOmmand, then cancels the ICommand
		 * and sets the references to null.
		 */
		private function reset():void {

			if(currentCmd != null) {
				currentCmd.removeEventListener(ProcessEvent.COMPLETED, doNext);
				currentCmd.removeEventListener(ProcessEvent.FAILED, onCommandFailed);
				currentCmd.cancel();
				currentCmd = null;
			}
		}

		private function getClass(obj:Object):Class {
			return Class(getDefinitionByName(getQualifiedClassName(obj)));
		}
	}
}